/*
 * #%L
 * Alfresco Remote API
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.web.scripts.calendar;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.alfresco.repo.calendar.CalendarModel;
import org.alfresco.service.cmr.calendar.CalendarEntry;
import org.alfresco.service.cmr.calendar.CalendarRecurrenceHelper;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.ISO8601DateFormat;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

/**
 * This class provides functionality common across the webscripts
 *  which list events.
 * 
 * @author Nick Burch
 * @since 4.0
 */
public abstract class AbstractCalendarListingWebScript extends AbstractCalendarWebScript
{
   protected static final String RESULT_EVENT = "event"; 
   protected static final String RESULT_NAME  = "name"; 
   protected static final String RESULT_TITLE = "title"; 
   protected static final String RESULT_START = "start"; 
   protected static final String RESULT_END = "end"; 
   
   /**
    * Returns a Comparator for (re-)sorting events, typically used after
    *  expanding out recurring instances.
    */
   protected static Comparator<Map<String, Object>> getEventDetailsSorter()
   {
      return new Comparator<Map<String, Object>>() 
      {
         public int compare(Map<String, Object> resultA,
               Map<String, Object> resultB) 
         {
        	 DateTimeFormatter fmtNoTz  = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS");
        	 DateTimeFormatter fmtTz  =   DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZ");
        	 
        	 String startA = (String)resultA.get(RESULT_START);
        	 String startB = (String)resultB.get(RESULT_START);
        
        	 startA = startA.replace("Z", "+00:00");
        	 startB = startB.replace("Z", "+00:00");
        	 
        	 //check and parse iso8601 date without time zone (All day events are stripped of time zone)
        	 DateTime sa = startA.length()>23?fmtTz.parseDateTime(startA):fmtNoTz.parseDateTime(startA);
        	 DateTime sb = startB.length()>23?fmtTz.parseDateTime(startB):fmtNoTz.parseDateTime(startB);

            int cmp = sa.compareTo(sb);
            if (cmp == 0)
            {
            	String endA = (String)resultA.get(RESULT_END);
            	String endB = (String)resultB.get(RESULT_END);
            	
            	DateTime ea = endA.length()>23?fmtTz.parseDateTime(endA):fmtNoTz.parseDateTime(endA);
            	DateTime eb = endB.length()>23?fmtTz.parseDateTime(endB):fmtNoTz.parseDateTime(endB);

               cmp = ea.compareTo(eb);
               if (cmp == 0)
               {
                  String nameA = (String)resultA.get(RESULT_NAME);
                  String nameB = (String)resultB.get(RESULT_NAME);
                  return nameA.compareTo(nameB);
               }
               return cmp;
            }
            return cmp;
         }
      };
   }

   /**
    * Do what's needed for recurring events.
    * 
    * @return If dates have been tweaked, and a sort may be required 
    */
   protected boolean handleRecurring(CalendarEntry entry, Map<String, Object> entryResult, 
         List<Map<String, Object>> allResults, Date from, Date until, boolean repeatingFirstOnly)
   {
      if (entry.getRecurrenceRule() == null)
      {
         // Nothing to do
         return false;
      }
      
      // If no date is given, start looking for occurrences from the event itself
      if (from == null)
      {
         from = entry.getStart();
      }
      
      // Do we need to limit ourselves?
      // Should we limit ourselves?
      if (!repeatingFirstOnly)
      {
         if (until == null)
         {
            // If no end date was given, only allow repeating instances 
            // for next 60 days, to keep the list sane
            // (It's normally only used for a month view anyway)
            Calendar c = Calendar.getInstance();
            c.setTime(from);
            c.add(Calendar.DATE, 60);
            until = c.getTime();
         }
      }
      
      // How long is it?
      long duration = entry.getEnd().getTime() - entry.getStart().getTime();
      
      // if some instances were deleted from series ignore them
      Set<QName> childNodeTypeQNames = new HashSet<QName>();
      childNodeTypeQNames.add(CalendarModel.TYPE_IGNORE_EVENT);
      List<ChildAssociationRef> ignoreEventList = nodeService.getChildAssocs(entry.getNodeRef(), childNodeTypeQNames);
      Set<Date> ignoredDates = new HashSet<Date>();
      for (ChildAssociationRef ignoreEvent : ignoreEventList)
      {
          NodeRef nodeRef = ignoreEvent.getChildRef();
          Date ignoredDate = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_IGNORE_EVENT_DATE);
          ignoredDates.add(ignoredDate);
      }
      
      // Get it's recurring instances
      List<Date> dates = CalendarRecurrenceHelper.getRecurrencesOnOrAfter(
            entry, from, until, repeatingFirstOnly, ignoredDates);
      if (dates == null)
      {
         dates = new ArrayList<Date>();
      }
      
      // Add on the original event time itself if needed
      if (entry.getStart().getTime() >= from.getTime())
      {
         if (dates.size() == 0 || dates.get(0).getTime() != entry.getStart().getTime())
         {
            // Original event is after the start time, and not on the recurring list
            dates.add(0, entry.getStart());
         }
      }
      
      // If we got no dates, then no recurrences in the period so zap
      if (dates.size() == 0)
      {
         allResults.remove(entryResult);
         return false; // Remains sorted despite delete
      }

      // if some instances were updated
      SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");
      childNodeTypeQNames = new HashSet<QName>();
      childNodeTypeQNames.add(CalendarModel.TYPE_UPDATED_EVENT);
      List<ChildAssociationRef> updatedEventList = nodeService.getChildAssocs(entry.getNodeRef(), childNodeTypeQNames);
      Map<String, Object> updatedDates = new HashMap<String, Object>();
      for (ChildAssociationRef updatedEvent : updatedEventList)
      {
          NodeRef nodeRef = updatedEvent.getChildRef();
          Date updatedDate = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_EVENT_DATE);
          Date newStart = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_START);
          Date newEnd = (Date) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_END);
          String newWhere = (String) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_WHERE);
          String newWhat = (String) nodeService.getProperty(nodeRef, CalendarModel.PROP_UPDATED_WHAT);
          updatedDates.put(fmt.format(updatedDate), new Date[] { newStart, newEnd });
          updatedDates.put(fmt.format(updatedDate).toString() + "where", newWhere);
          updatedDates.put(fmt.format(updatedDate).toString() + "what", newWhat);
      }
      
      // first occurrence can be edited as separate event
      Date liveEntry = dates.get(0);
      
      // If first result only, alter title and finish
      if (repeatingFirstOnly)
      {
         entryResult.put(RESULT_TITLE, entry.getTitle() + " (Repeating)");
         
         updateRepeating(entry, updatedDates, entryResult, duration, fmt, liveEntry);
         return true; // Date has been changed
      }
      else
      {
         // Otherwise generate one entry per extra date
         for (int i = 1; i < dates.size(); i++)
         {
            // Clone the properties
            Map<String, Object> newResult = new HashMap<String, Object>(entryResult);
            
            Date extra = dates.get(i);
            
            updateRepeating(entry, updatedDates, newResult, duration, fmt, extra);
            
            // Save as a new event
            allResults.add(newResult);
         }
         
         updateRepeating(entry, updatedDates, entryResult, duration, fmt, liveEntry);
      }
      
      // TODO Skip ignored instances
      
      // New dates have been added
      return true;
   }
   
   private void updateRepeatingStartEnd(Date newStart, long duration, Map<String, Object> result)
   {
      Date newEnd = new Date(newStart.getTime() + duration);
      result.put(RESULT_START,  ISO8601DateFormat.format(newStart));
      result.put(RESULT_END, ISO8601DateFormat.format(newEnd));
      String legacyDateFormat = "yyyy-MM-dd";
      SimpleDateFormat ldf = new SimpleDateFormat(legacyDateFormat);
      String legacyTimeFormat ="HH:mm";
      SimpleDateFormat ltf = new SimpleDateFormat(legacyTimeFormat);
      result.put("legacyDateFrom", ldf.format(newStart));
      result.put("legacyTimeFrom", ltf.format(newStart));
      result.put("legacyDateTo", ldf.format(newEnd));
      result.put("legacyTimeTo", ltf.format(newEnd));
   }
   
   private void updateRepeating(CalendarEntry entry, Map<String, Object> updatedDates, Map<String, Object> entryResult, long duration, SimpleDateFormat fmt, Date date)
   {
      if (updatedDates.keySet().contains(fmt.format(date)))
      {
         // there is day that was edited
         Date[] newValues = (Date[]) updatedDates.get(fmt.format(date));
         long newDuration = newValues[1].getTime() - newValues[0].getTime();

         entryResult.put(RESULT_TITLE, (String) updatedDates.get(fmt.format(date).toString() + "what"));
         entryResult.put("where", (String) updatedDates.get(fmt.format(date).toString() + "where"));
         
         updateRepeatingStartEnd(newValues[0], newDuration, entryResult);
      }
      else
      {
         // Update entry
         updateRepeatingStartEnd(date, duration, entryResult);
      }
   }
}
